<script setup lang="ts">
import {
  Layouts,
  type ReferenceConfiguration,
  useAuthenticationStore,
  useDarkModeState,
  useHttpClientStore,
  useReactiveSpec,
} from '@scalar/api-reference'
import '@scalar/api-reference/style.css'
import { fetchSpecFromUrl } from '@scalar/oas-utils/helpers'
import type { SpecConfiguration } from '@scalar/types/legacy'
import { createHead, useSeoMeta } from 'unhead'
import { computed, ref, toRef, watch, watchEffect } from 'vue'

import EditorInput from './EditorInput.vue'

const props = defineProps<{
  configuration?: ReferenceConfiguration & {
    /** Option to manage the state externally and have the spec reactively update  */
    useExternalState?: boolean
  }
}>()

// If content changes it is emitted to the parent component to be handed back in as a spec string
defineEmits<{
  (e: 'updateContent', value: string): void
}>()

const { toggleDarkMode, isDark } = useDarkModeState(
  props.configuration?.darkMode,
)

const editorContent = ref('')

/** Set the new value of the spec file in the input */
function setSpec({ url, content }: SpecConfiguration) {
  if (url) {
    // For URLs we just set the value to fetched string
    fetchSpecFromUrl(url).then((val) => {
      editorContent.value = val
    })
  } else if (typeof content === 'string') {
    // For string values we don't want to stringify as it will alter the format
    editorContent.value = content
  } else {
    // For object specs we must stringify
    editorContent.value = content
      ? JSON.stringify(typeof content === 'function' ? content() : content)
      : ''
  }
}

// Set the content whenever the input props change
watchEffect(() => {
  setSpec(props.configuration?.spec ?? { content: '' })
})

// ---------------------------------------------------------------------------
/** Editor input used to emit the custom event */
const inputRef = ref<InstanceType<typeof EditorInput> | null>(null)

/** When using external state we have to dispatch the custom event */
function handleContentChange(value: string) {
  if (props.configuration?.useExternalState) {
    /** Custom event (scalar-update) to emit when content changes */
    inputRef.value?.dispatchUpdate(value)
  } else {
    editorContent.value = value
  }
}

/** When using internal state we capture the event and assign the value */
function handleInput(evt: CustomEvent<{ value: string }>) {
  if (!props.configuration?.useExternalState)
    editorContent.value = evt.detail.value
}

// ---------------------------------------------------------------------------

// Set defaults as needed on the provided configuration
const configuration = computed<ReferenceConfiguration>(() => {
  return {
    theme: 'default',
    showSidebar: true,
    isEditable: true,
    ...props.configuration,
    /** If we are managing the dynamic state internally we override the spec */
    spec: props.configuration?.useExternalState
      ? props.configuration.spec
      : { content: editorContent.value },
  }
})

// Create the head tag if the configuration has meta data
if (configuration.value?.metaData) {
  createHead()
  useSeoMeta(configuration.value.metaData)
}

// ---------------------------------------------------------------------------/
// HANDLE MAPPING CONFIGURATION TO INTERNAL REFERENCE STATE

/** Helper utility to map configuration props to the ApiReference internal state */
function mapConfigToState<K extends keyof ReferenceConfiguration>(
  key: K,
  setter: (val: NonNullable<ReferenceConfiguration[K]>) => any,
) {
  watch(
    () => configuration.value?.[key],
    (newValue) => {
      if (typeof newValue !== 'undefined') setter(newValue)
    },
    { immediate: true },
  )
}

// Prefill authentication
const { setAuthentication } = useAuthenticationStore()
mapConfigToState('authentication', setAuthentication)

// Hides any client snippets from the references
const { setExcludedClients } = useHttpClientStore()
mapConfigToState('hiddenClients', setExcludedClients)

const { parsedSpec, rawSpec } = useReactiveSpec({
  proxy: toRef(() => configuration.value.proxy || ''),
  specConfig: toRef(() => configuration.value.spec || {}),
})
</script>
<template>
  <!-- Inject any custom CSS directly into a style tag -->
  <component
    :is="'style'"
    v-if="configuration?.customCss">
    {{ configuration.customCss }}
  </component>
  <Layouts
    :configuration="configuration"
    :isDark="isDark"
    :parsedSpec="parsedSpec"
    :rawSpec="rawSpec"
    @toggleDarkMode="() => toggleDarkMode()"
    @updateContent="handleContentChange">
    <template #editor>
      <!--  -->
      <EditorInput
        ref="inputRef"
        :modelValue="editorContent"
        @scalarUpdate="handleInput" />
    </template>
    <!-- Expose the content end slot as a slot for the footer -->
    <template #content-end><slot name="footer" /></template>
  </Layouts>
</template>
<style>
body {
  margin: 0;
}
</style>
